<html lang="en">
	<head>
		<title>Lebarba - WebGL Volume Rendering made easy</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<style>
			body {
				color: #ffffff;
				font-family:Monospace;
				font-size:13px;
				text-align:center;
				font-weight: bold;

				background-color: #050505;
				margin: 0px;
				overflow: hidden;
			}

			#info {
				position: absolute;
				top: 0px; width: 100%;
				padding: 5px;
			}

			a {
				color: #ffffff;
			}

			#oldie a { color:#da0 }
		</style>
	</head>
	<body>
		<div id="container">
			<div>Transfer function</div>
			0.0<img id="transferFunctionImg" style="align:right"/>1.0
		</div>
		<script src="./js/three.min.js"></script>
		<script src="./js/Detector.js"></script>
		<script src="./js/stats.min.js"></script>
		<script src="./js/OrbitControls.js"></script>
		<script src="./js/dat.gui.min.js"></script>

		<script id="fragmentShaderFirstPass" type="x-shader/x-fragment">
			varying vec3 worldSpaceCoords;

			void main()
			{
				//The fragment's world space coordinates as fragment output.
				gl_FragColor = vec4( worldSpaceCoords.x, worldSpaceCoords.y, worldSpaceCoords.z, 1 );
			}
		</script>
		<script id="vertexShaderFirstPass" type="x-shader/x-vertex">
			varying vec3 worldSpaceCoords;
			
			uniform float boxWidth;
			uniform float boxHeight;
			uniform float boxDepth;

			void main()
			{
				//Set the world space coordinates of the back faces vertices as output.
				worldSpaceCoords = position + vec3(boxWidth / 2.0, boxHeight / 2.0, boxDepth / 2.0); //move it from [-0.5;0.5] to [0,1]
				gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
			}
		</script>
		<script id="fragmentShaderSecondPass" type="x-shader/x-fragment">
			varying vec3 worldSpaceCoords;
			varying vec4 projectedCoords;
			uniform sampler2D tex, cubeTex, transferTex;
			uniform float steps;
			uniform float alphaCorrection;
			uniform float boxWidth;
			uniform float boxHeight;
			uniform float boxDepth;
			// The maximum distance through our rendering volume is sqrt(3).
			// The maximum number of steps we take to travel a distance of 1 is 512.
			// ceil( sqrt(3) * 512 ) = 887
			// This prevents the back of the image from getting cut off when steps=512 & viewing diagonally.
			const int MAX_STEPS = 887;

			float linearScale(float domainStart, float domainEnd, float rangeStart, float rangeEnd, float value) {
				return ((rangeEnd - rangeStart) * (value - domainStart)) / (domainEnd - domainStart) + rangeStart;
			}

			const float slicesPerSide = 16.0;
			const float zDepth = slicesPerSide * slicesPerSide - 1.0;
			
			//Acts like a texture3D using Z slices and trilinear filtering.
			vec4 sampleAs3DTexture( vec3 texCoord )
			{
				texCoord.x = linearScale(0.0, boxWidth, 0.0, 1.0, texCoord.x);
				texCoord.y = linearScale(0.0, boxHeight, 0.0, 1.0, texCoord.y);

				vec4 colorSlice1, colorSlice2;
				vec2 texCoordSlice1, texCoordSlice2;

				//The z coordinate determines which Z slice we have to look for.
				//Z slice number goes from 0 to 255.
				float zSliceNumber1 = floor(texCoord.z * zDepth);

				//As we use trilinear we go the next Z slice.
				float zSliceNumber2 = min( zSliceNumber1 + 1.0, zDepth); //Clamp to 255

				//The Z slices are stored in a matrix of 16x16 of Z slices.
				//The original UV coordinates have to be rescaled by the tile numbers in each row and column.
				texCoord.xy /= slicesPerSide;

				texCoordSlice1 = texCoordSlice2 = texCoord.xy;

				//Add an offset to the original UV coordinates depending on the row and column number.
				texCoordSlice1.x += (mod(zSliceNumber1, slicesPerSide ) / slicesPerSide);
				texCoordSlice1.y += floor((zDepth - zSliceNumber1) / slicesPerSide) / slicesPerSide;

				texCoordSlice2.x += (mod(zSliceNumber2, slicesPerSide ) / slicesPerSide);
				texCoordSlice2.y += floor((zDepth - zSliceNumber2) / slicesPerSide) / slicesPerSide;

				//Get the opacity value from the 2D texture.
				//Bilinear filtering is done at each texture2D by default.
				colorSlice1 = texture2D( cubeTex, texCoordSlice1 );
				colorSlice2 = texture2D( cubeTex, texCoordSlice2 );

				//Based on the opacity obtained earlier, get the RGB color in the transfer function texture.
				colorSlice1.rgb = texture2D( transferTex, vec2( colorSlice1.a, 1.0) ).rgb;
				colorSlice2.rgb = texture2D( transferTex, vec2( colorSlice2.a, 1.0) ).rgb;

				//How distant is zSlice1 to ZSlice2. Used to interpolate between one Z slice and the other.
				float zDifference = mod(texCoord.z * zDepth, 1.0);

				//Finally interpolate between the two intermediate colors of each Z slice.
				return mix(colorSlice1, colorSlice2, zDifference);
			}


			void main( void ) {

				//Transform the coordinates it from [-1;1] to [0;1]
				vec2 texc = vec2(((projectedCoords.x / projectedCoords.w) + 1.0 ) / 2.0,
								((projectedCoords.y / projectedCoords.w) + 1.0 ) / 2.0 );

				//The back position is the world space position stored in the texture.
				vec3 backPos = texture2D(tex, texc).xyz;

				//The front position is the world space position of the second render pass.
				vec3 frontPos = worldSpaceCoords;

				//Using NearestFilter for rtTexture mostly eliminates bad backPos values at the edges
				//of the cube, but there may still be no valid backPos value for the current fragment.
				if ((backPos.x == 0.0) && (backPos.y == 0.0))
				{
					gl_FragColor = vec4(0.0);
					return;
				}

				//The direction from the front position to back position.
				vec3 dir = backPos - frontPos;

				float rayLength = length(dir);

				//Calculate how long to increment in each step.
				float delta = 1.0 / steps;

				//The increment in each direction for each step.
				vec3 deltaDirection = normalize(dir) * delta;
				float deltaDirectionLength = length(deltaDirection);

				//Start the ray casting from the front position.
				vec3 currentPosition = frontPos;

				//The color accumulator.
				vec4 accumulatedColor = vec4(0.0);

				//The alpha value accumulated so far.
				float accumulatedAlpha = 0.0;

				//How long has the ray travelled so far.
				float accumulatedLength = 0.0;

				//If we have twice as many samples, we only need ~1/2 the alpha per sample.
				//Scaling by 256/10 just happens to give a good value for the alphaCorrection slider.
				float alphaScaleFactor = 25.6 * delta;

				vec4 colorSample;
				float alphaSample;

				//Perform the ray marching iterations
				for(int i = 0; i < MAX_STEPS; i++)
				{
					//Get the voxel intensity value from the 3D texture.
					colorSample = sampleAs3DTexture( currentPosition );

					//Allow the alpha correction customization.
					alphaSample = colorSample.a * alphaCorrection;

					//Applying this effect to both the color and alpha accumulation results in more realistic transparency.
					alphaSample *= (1.0 - accumulatedAlpha);

					//Scaling alpha by the number of steps makes the final color invariant to the step size.
					alphaSample *= alphaScaleFactor;

					//Perform the composition.
					accumulatedColor += colorSample * alphaSample;

					//Store the alpha accumulated so far.
					accumulatedAlpha += alphaSample;

					//Advance the ray.
					currentPosition += deltaDirection;
					accumulatedLength += deltaDirectionLength;

					//If the length traversed is more than the ray length, or if the alpha accumulated reaches 1.0 then exit.
					if(accumulatedLength >= rayLength || accumulatedAlpha >= 1.0 )
						break;
				}

				gl_FragColor  = accumulatedColor;

			}
		</script>

		<script id="vertexShaderSecondPass" type="x-shader/x-vertex">
			varying vec3 worldSpaceCoords;
			varying vec4 projectedCoords;
			
			uniform float boxWidth;
			uniform float boxHeight;
			uniform float boxDepth;
	
			void main()
			{
				worldSpaceCoords = (modelMatrix * vec4(position + vec3(boxWidth / 2.0, boxHeight / 2.0, boxDepth / 2.0), 1.0 )).xyz;
				gl_Position = projectionMatrix *  modelViewMatrix * vec4( position, 1.0 );
				projectedCoords =  projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
			}
		</script>

		<script>
			if ( ! Detector.webgl ) Detector.addGetWebGLMessage();

			var container, stats;
			var camera, sceneFirstPass, sceneSecondPass, renderer;

			var clock = new THREE.Clock();
			var rtTexture, transferTexture;
			var cubeTextures = ['bonsai', 'foot', 'teapot'];
			var histogram = [];
			var guiControls;

			var materialSecondPass;
			init();
			animate();

			function init() {

				//Parameters that can be modified.
				guiControls = new function() {
					this.model = 'bonsai';
					this.steps = 256.0;
					this.alphaCorrection = 1.0;
					this.color1 = "#00FA58";
					this.stepPos1 = 0.1;
					this.color2 = "#CC6600";
					this.stepPos2 = 0.7;
					this.color3 = "#F2F200";
					this.stepPos3 = 1.0;
				};

				container = document.getElementById( 'container' );

				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.01, 3000.0 );
				camera.position.z = 2.0;

				controls = new THREE.OrbitControls( camera, container );
				controls.target.set( 0.0, 0.0, 0.0 );


				//Load the 2D texture containing the Z slices.
				cubeTextures["bonsai"] = new THREE.TextureLoader().load("bonsai.raw.png");
				cubeTextures["teapot"] = new THREE.TextureLoader().load("teapot.raw.png");
				cubeTextures["foot"] = new THREE.TextureLoader().load("foot.raw.png");

				//Don't let it generate mipmaps to save memory and apply linear filtering to prevent use of LOD.
				cubeTextures['bonsai'].generateMipmaps = false;
				cubeTextures['bonsai'].minFilter = THREE.LinearFilter;
				cubeTextures['bonsai'].magFilter = THREE.LinearFilter;

				cubeTextures['teapot'].generateMipmaps = false;
				cubeTextures['teapot'].minFilter = THREE.LinearFilter;
				cubeTextures['teapot'].magFilter = THREE.LinearFilter;

				cubeTextures['foot'].generateMipmaps = false;
				cubeTextures['foot'].minFilter = THREE.LinearFilter;
				cubeTextures['foot'].magFilter = THREE.LinearFilter;


				var transferTexture = updateTransferFunction();

				var screenSize = new THREE.Vector2( window.innerWidth, window.innerHeight );
				//Use NearestFilter to eliminate interpolation.  At the cube edges, interpolated world coordinates
				//will produce bogus ray directions in the fragment shader, and thus extraneous colors.
				rtTexture = new THREE.WebGLRenderTarget(screenSize.x, screenSize.y, {
					minFilter: THREE.NearestFilter,
					magFilter: THREE.NearestFilter,
					wrapS: THREE.ClampToEdgeWrapping,
					wrapT: THREE.ClampToEdgeWrapping,
					format: THREE.RGBAFormat,
					type: THREE.FloatType,
					generateMipmaps: false,
					stencilBuffer: true,
				});

				// Normalized sizes of the cube's dimensions.
				// Format: [x, y, z] or [width, height, depth]
				// If the input data "cube" has dimensions 50 x 25 x 100, then boxSize should be [0.5, 0.25, 1.0]
				// Hardcoding to [1 ,1, 1] here because all input dataets are cubes. 
				const boxSize = [1, 1, 1]

				var materialFirstPass = new THREE.ShaderMaterial( {
					vertexShader: document.getElementById( 'vertexShaderFirstPass' ).textContent,
					fragmentShader: document.getElementById( 'fragmentShaderFirstPass' ).textContent,
					side: THREE.BackSide,
					uniforms: {
                        boxWidth: {
                            type: '1f',
                            value: boxSize[0],
                        },
                        boxHeight: {
                            type: '1f',
                            value: boxSize[1],
                        },
                        boxDepth: {
                            type: '1f',
                            value: boxSize[2],
                        },
                    },
				} );

				materialSecondPass = new THREE.ShaderMaterial( {
					vertexShader: document.getElementById( 'vertexShaderSecondPass' ).textContent,
					fragmentShader: document.getElementById( 'fragmentShaderSecondPass' ).textContent,
					side: THREE.FrontSide,
					uniforms: {	tex:  { type: "t", value: rtTexture.texture },
								cubeTex:  { type: "t", value: cubeTextures['bonsai'] },
								transferTex:  { type: "t", value: transferTexture },
								steps : {type: "1f" , value: guiControls.steps },
								alphaCorrection : {type: "1f", value: guiControls.alphaCorrection },
								boxWidth: {type: '1f', value: boxSize[0] },
								boxHeight: {type: '1f', value: boxSize[1] },
								boxDepth: {type: '1f', value: boxSize[2] }
					}
				} );
				materialSecondPass.needsUpdate = true;

				sceneFirstPass = new THREE.Scene();
				sceneSecondPass = new THREE.Scene();

				var boxGeometry = new THREE.BoxGeometry(1.0, 1.0, 1.0);
				boxGeometry.doubleSided = true;

				var meshFirstPass = new THREE.Mesh( boxGeometry, materialFirstPass );
				var meshSecondPass = new THREE.Mesh( boxGeometry, materialSecondPass );

				sceneFirstPass.add( meshFirstPass );
				sceneSecondPass.add( meshSecondPass );

				renderer = new THREE.WebGLRenderer();
				container.appendChild( renderer.domElement );

				stats = new Stats();
				stats.domElement.style.position = 'absolute';
				stats.domElement.style.top = '0px';
				container.appendChild( stats.domElement );


				var gui = new dat.GUI();
				var modelSelected = gui.add(guiControls, 'model', [ 'bonsai', 'foot', 'teapot' ] );
				gui.add(guiControls, 'steps', 0.0, 512.0);
				gui.add(guiControls, 'alphaCorrection', 0.01, 5.0).step(0.01);

				modelSelected.onChange(function(value) { materialSecondPass.uniforms.cubeTex.value =  cubeTextures[value]; } );


				//Setup transfer function steps.
				var step1Folder = gui.addFolder('Step 1');
				var controllerColor1 = step1Folder.addColor(guiControls, 'color1');
				var controllerStepPos1 = step1Folder.add(guiControls, 'stepPos1', 0.0, 1.0);
				controllerColor1.onChange(updateTextures);
				controllerStepPos1.onChange(updateTextures);

				var step2Folder = gui.addFolder('Step 2');
				var controllerColor2 = step2Folder.addColor(guiControls, 'color2');
				var controllerStepPos2 = step2Folder.add(guiControls, 'stepPos2', 0.0, 1.0);
				controllerColor2.onChange(updateTextures);
				controllerStepPos2.onChange(updateTextures);

				var step3Folder = gui.addFolder('Step 3');
				var controllerColor3 = step3Folder.addColor(guiControls, 'color3');
				var controllerStepPos3 = step3Folder.add(guiControls, 'stepPos3', 0.0, 1.0);
				controllerColor3.onChange(updateTextures);
				controllerStepPos3.onChange(updateTextures);

				step1Folder.open();
				step2Folder.open();
				step3Folder.open();


				onWindowResize();

				window.addEventListener( 'resize', onWindowResize, false );

			}

			function updateTextures(value)
			{
				materialSecondPass.uniforms.transferTex.value = updateTransferFunction();
			}
			function updateTransferFunction()
			{
				var canvas = document.createElement('canvas');
				canvas.height = 20;
				canvas.width = 256;

				var ctx = canvas.getContext('2d');

				var grd = ctx.createLinearGradient(0, 0, canvas.width -1 , canvas.height - 1);
				grd.addColorStop(guiControls.stepPos1, guiControls.color1);
				grd.addColorStop(guiControls.stepPos2, guiControls.color2);
				grd.addColorStop(guiControls.stepPos3, guiControls.color3);

				ctx.fillStyle = grd;
				ctx.fillRect(0,0,canvas.width -1 ,canvas.height -1 );

				var img = document.getElementById("transferFunctionImg");
				img.src = canvas.toDataURL();
				img.style.width = "256 px";
				img.style.height = "128 px";

				transferTexture =  new THREE.Texture(canvas);
				transferTexture.wrapS = transferTexture.wrapT =  THREE.ClampToEdgeWrapping;
				transferTexture.needsUpdate = true;

				return transferTexture;
			}

			function onWindowResize( event ) {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );
			}

			function animate() {

				requestAnimationFrame( animate );

				render();
				stats.update();
			}

			function render() {

				var delta = clock.getDelta();

				//Render first pass and store the world space coords of the back face fragments into the texture.
				renderer.setRenderTarget(rtTexture);
				renderer.render(sceneFirstPass, camera);
				renderer.setRenderTarget(null);

				//Render the second pass and perform the volume rendering.
				renderer.render(sceneSecondPass, camera);

				materialSecondPass.uniforms.steps.value = guiControls.steps;
				materialSecondPass.uniforms.alphaCorrection.value = guiControls.alphaCorrection;
			}

			//Leandro R Barbagallo - 2015 - lebarba at gmail.com
		</script>

	</body>
</html>
